// ==========================================================
// JPEG XR Loader & Writer
//
// Design and implementation by
// - Herve Drolon (drolon@infonie.fr)
//
// This file is part of FreeImage 3
//
// COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES
// THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE
// OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED
// CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT
// THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY
// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL
// PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER
// THIS DISCLAIMER.
//
// Use at your own risk!
// ==========================================================

#include "FreeImage.h"
#include "Utilities.h"
#include "../Metadata/FreeImageTag.h"

#include "../LibJXR/jxrgluelib/JXRGlue.h"

// ==========================================================
// Plugin Interface
// ==========================================================

static int s_format_id;

// ==========================================================
// FreeImageIO interface (I/O streaming functions)
// ==========================================================

/**
   JXR wrapper for FreeImage I/O handle
*/
typedef struct tagFreeImageJXRIO {
    FreeImageIO *io;
    fi_handle handle;
} FreeImageJXRIO;

static ERR
_jxr_io_Read(WMPStream* pWS, void* pv, size_t cb) {
    FreeImageJXRIO *fio = (FreeImageJXRIO*)pWS->state.pvObj;
    return (fio->io->read_proc(pv, (unsigned)cb, 1, fio->handle) == 1) ? WMP_errSuccess : WMP_errFileIO;
}

static ERR
_jxr_io_Write(WMPStream* pWS, const void* pv, size_t cb) {
    FreeImageJXRIO *fio = (FreeImageJXRIO*)pWS->state.pvObj;
    if(0 != cb) {
        return (fio->io->write_proc((void*)pv, (unsigned)cb, 1, fio->handle) == 1) ? WMP_errSuccess : WMP_errFileIO;
    }
    return WMP_errFileIO;
}

static ERR
_jxr_io_SetPos(WMPStream* pWS, size_t offPos) {
    FreeImageJXRIO *fio = (FreeImageJXRIO*)pWS->state.pvObj;
    return (fio->io->seek_proc(fio->handle, (long)offPos, SEEK_SET) == 0) ? WMP_errSuccess : WMP_errFileIO;
}

static ERR
_jxr_io_GetPos(WMPStream* pWS, size_t* poffPos) {
    FreeImageJXRIO *fio = (FreeImageJXRIO*)pWS->state.pvObj;
    long lOff = fio->io->tell_proc(fio->handle);
    if(lOff == -1) {
        return WMP_errFileIO;
    }
    *poffPos = (size_t)lOff;
    return WMP_errSuccess;
}

static Bool
_jxr_io_EOS(WMPStream* pWS) {
    FreeImageJXRIO *fio = (FreeImageJXRIO*)pWS->state.pvObj;
    long currentPos = fio->io->tell_proc(fio->handle);
    fio->io->seek_proc(fio->handle, 0, SEEK_END);
    long fileRemaining = fio->io->tell_proc(fio->handle) - currentPos;
    fio->io->seek_proc(fio->handle, currentPos, SEEK_SET);
    return (fileRemaining > 0);
}

static ERR
_jxr_io_Close(WMPStream** ppWS) {
    WMPStream *pWS = *ppWS;
    // HACK : we use fMem to avoid a stream destruction by the library
    // because FreeImage MUST HAVE the ownership of the stream
    // see _jxr_io_Create
    if(pWS && pWS->fMem) {
        free(pWS);
        *ppWS = NULL;
    }
    return WMP_errSuccess;
}

static ERR
_jxr_io_Create(WMPStream **ppWS, FreeImageJXRIO *jxr_io) {
    *ppWS = (WMPStream*)calloc(1, sizeof(**ppWS));
    if(*ppWS) {
        WMPStream *pWS = *ppWS;

        pWS->state.pvObj = jxr_io;
        pWS->Close = _jxr_io_Close;
        pWS->EOS = _jxr_io_EOS;
        pWS->Read = _jxr_io_Read;
        pWS->Write = _jxr_io_Write;
        pWS->SetPos = _jxr_io_SetPos;
        pWS->GetPos = _jxr_io_GetPos;

        // HACK : we use fMem to avoid a stream destruction by the library
        // because FreeImage MUST HAVE the ownership of the stream
        // see _jxr_io_Close
        pWS->fMem = FALSE;

        return WMP_errSuccess;
    }
    return WMP_errOutOfMemory;
}

// ==========================================================
// JPEG XR Error handling
// ==========================================================

static const char*
JXR_ErrorMessage(const int error) {
    switch(error) {
        case WMP_errNotYetImplemented:
        case WMP_errAbstractMethod:
            return "Not yet implemented";
        case WMP_errOutOfMemory:
            return "Out of memory";
        case WMP_errFileIO:
            return "File I/O error";
        case WMP_errBufferOverflow:
            return "Buffer overflow";
        case WMP_errInvalidParameter:
            return "Invalid parameter";
        case WMP_errInvalidArgument:
            return "Invalid argument";
        case WMP_errUnsupportedFormat:
            return "Unsupported format";
        case WMP_errIncorrectCodecVersion:
            return "Incorrect codec version";
        case WMP_errIndexNotFound:
            return "Format converter: Index not found";
        case WMP_errOutOfSequence:
            return "Metadata: Out of sequence";
        case WMP_errMustBeMultipleOf16LinesUntilLastCall:
            return "Must be multiple of 16 lines until last call";
        case WMP_errPlanarAlphaBandedEncRequiresTempFile:
            return "Planar alpha banded encoder requires temp files";
        case WMP_errAlphaModeCannotBeTranscoded:
            return "Alpha mode cannot be transcoded";
        case WMP_errIncorrectCodecSubVersion:
            return "Incorrect codec subversion";
        case WMP_errFail:
        case WMP_errNotInitialized:
        default:
            return "Invalid instruction - please contact the FreeImage team";
    }
}

// ==========================================================
// Helper functions & macro
// ==========================================================

#define JXR_CHECK(error_code)                                       \
    if(error_code < 0) {                                            \
        const char *error_message = JXR_ErrorMessage(error_code);   \
        throw error_message;                                        \
    }

// --------------------------------------------------------------------------

/**
   Input conversions natively understood by FreeImage
   @see GetNativePixelFormat
*/
typedef struct tagJXRInputConversion {
    BITDEPTH_BITS bdBitDepth;
    U32 cbitUnit;
    FREE_IMAGE_TYPE image_type;
    unsigned red_mask;
    unsigned green_mask;
    unsigned blue_mask;
} JXRInputConversion;

/**
   Conversion table for native FreeImage formats
   @see GetNativePixelFormat
*/
static JXRInputConversion s_FreeImagePixelInfo[] = {
    // 1-bit bitmap
    { BD_1, 1, FIT_BITMAP, 0, 0, 0 },
    // 8-, 24-, 32-bit bitmap
    { BD_8, 8, FIT_BITMAP, 0, 0, 0 },
    { BD_8, 24, FIT_BITMAP, 0, 0, 0 },
    { BD_8, 32, FIT_BITMAP, 0, 0, 0 },
    // 16-bit RGB 565
    { BD_565, 16, FIT_BITMAP, FI16_565_RED_MASK, FI16_565_GREEN_MASK, FI16_565_BLUE_MASK },
    // 16-bit RGB 555
    { BD_5, 16, FIT_BITMAP, FI16_555_RED_MASK, FI16_555_GREEN_MASK, FI16_555_BLUE_MASK },
    // 16-bit greyscale, RGB16, RGBA16 bitmap
    { BD_16, 16, FIT_UINT16, 0, 0, 0 },
    { BD_16, 48, FIT_RGB16, 0, 0, 0 },
    { BD_16, 64, FIT_RGBA16, 0, 0, 0 },
    // 32-bit float, RGBF, RGBAF bitmap
    { BD_32F, 32, FIT_FLOAT, 0, 0, 0 },
    { BD_32F, 96, FIT_RGBF, 0, 0, 0 },
    { BD_32F, 128, FIT_RGBAF, 0, 0, 0 }
};

/**
   Scan input pixelInfo specifications and return the equivalent FreeImage info for loading
   @param pixelInfo Image specifications
   @param out_guid_format (returned value) output pixel format
   @param image_type (returned value) Image type
   @param bpp (returned value) Image bit depth
   @param red_mask (returned value) RGB mask
   @param green_mask (returned value) RGB mask
   @param blue_mask (returned value) RGB mask
   @return Returns WMP_errSuccess if successful, returns WMP_errFail otherwise
   @see GetInputPixelFormat
*/
static ERR
GetNativePixelFormat(const PKPixelInfo *pixelInfo, PKPixelFormatGUID *out_guid_format, FREE_IMAGE_TYPE *image_type, unsigned *bpp, unsigned *red_mask, unsigned *green_mask, unsigned *blue_mask) {
    const unsigned s_FreeImagePixelInfoSize = (unsigned)sizeof(s_FreeImagePixelInfo) / sizeof(*(s_FreeImagePixelInfo));

    for(unsigned i = 0; i < s_FreeImagePixelInfoSize; i++) {
        if(pixelInfo->bdBitDepth == s_FreeImagePixelInfo[i].bdBitDepth) {
            if(pixelInfo->cbitUnit == s_FreeImagePixelInfo[i].cbitUnit) {
                // found ! now get dst image format specifications
                memcpy(out_guid_format, pixelInfo->pGUIDPixFmt, sizeof(PKPixelFormatGUID));
                *image_type = s_FreeImagePixelInfo[i].image_type;
                *bpp = s_FreeImagePixelInfo[i].cbitUnit;
                *red_mask   = s_FreeImagePixelInfo[i].red_mask;
                *green_mask = s_FreeImagePixelInfo[i].green_mask;
                *blue_mask  = s_FreeImagePixelInfo[i].blue_mask;
                return WMP_errSuccess;
            }
        }
    }

    // not found : need pixel format conversion
    return WMP_errFail;
}

/**
   Scan input file guid format and return the equivalent FreeImage info & target guid format for loading
   @param pDecoder Decoder handle
   @param guid_format (returned value) Output pixel format
   @param image_type (returned value) Image type
   @param bpp (returned value) Image bit depth
   @param red_mask (returned value) RGB mask
   @param green_mask (returned value) RGB mask
   @param blue_mask (returned value) RGB mask
   @return Returns TRUE if successful, returns FALSE otherwise
*/
static ERR
GetInputPixelFormat(PKImageDecode *pDecoder, PKPixelFormatGUID *guid_format, FREE_IMAGE_TYPE *image_type, unsigned *bpp, unsigned *red_mask, unsigned *green_mask, unsigned *blue_mask) {
    ERR error_code = 0;     // error code as returned by the interface
    PKPixelInfo pixelInfo;  // image specifications

    try {
        // get input file pixel format ...
        PKPixelFormatGUID pguidSourcePF;
        error_code = pDecoder->GetPixelFormat(pDecoder, &pguidSourcePF);
        JXR_CHECK(error_code);
        pixelInfo.pGUIDPixFmt = &pguidSourcePF;
        // ... check for a supported format and get the format specifications
        error_code = PixelFormatLookup(&pixelInfo, LOOKUP_FORWARD);
        JXR_CHECK(error_code);

        // search for an equivalent native FreeImage format
        error_code = GetNativePixelFormat(&pixelInfo, guid_format, image_type, bpp, red_mask, green_mask, blue_mask);

        if(error_code != WMP_errSuccess) {
            // try to find a suitable conversion function ...
            const PKPixelFormatGUID *ppguidTargetPF = NULL; // target pixel format
            unsigned iIndex = 0;    // first available conversion function
            do {
                error_code = PKFormatConverter_EnumConversions(&pguidSourcePF, iIndex, &ppguidTargetPF);
                if(error_code == WMP_errSuccess) {
                    // found a conversion function, is the converted format a native FreeImage format ?
                    pixelInfo.pGUIDPixFmt = ppguidTargetPF;
                    error_code = PixelFormatLookup(&pixelInfo, LOOKUP_FORWARD);
                    JXR_CHECK(error_code);
                    error_code = GetNativePixelFormat(&pixelInfo, guid_format, image_type, bpp, red_mask, green_mask, blue_mask);
                    if(error_code == WMP_errSuccess) {
                        break;
                    }
                }
                // try next conversion function
                iIndex++;
            } while(error_code != WMP_errIndexNotFound);

        }

        return (error_code == WMP_errSuccess) ? WMP_errSuccess : WMP_errUnsupportedFormat;

    } catch(...) {
        return error_code;
    }
}

// --------------------------------------------------------------------------

/**
   Scan input dib format and return the equivalent PKPixelFormatGUID format for saving
   @param dib Image to be saved
   @param guid_format (returned value) GUID format
   @param bHasAlpha (returned value) TRUE if an alpha layer is present
   @return Returns TRUE if successful, returns FALSE otherwise
*/
static ERR
GetOutputPixelFormat(FIBITMAP *dib, PKPixelFormatGUID *guid_format, BOOL *bHasAlpha) {
    const FREE_IMAGE_TYPE image_type = FreeImage_GetImageType(dib);
    const unsigned bpp = FreeImage_GetBPP(dib);
    const FREE_IMAGE_COLOR_TYPE color_type = FreeImage_GetColorType(dib);

    *guid_format = GUID_PKPixelFormatDontCare;
    *bHasAlpha = FALSE;

    switch(image_type) {
        case FIT_BITMAP:    // standard image   : 1-, 4-, 8-, 16-, 24-, 32-bit
            switch(bpp) {
                case 1:
                    // assume FIC_MINISBLACK
                    if(color_type == FIC_MINISBLACK) {
                        *guid_format = GUID_PKPixelFormatBlackWhite;
                    }
                    break;
                case 8:
                    // assume FIC_MINISBLACK
                    if(color_type == FIC_MINISBLACK) {
                        *guid_format = GUID_PKPixelFormat8bppGray;
                    }
                    break;
                case 16:
                    if ((FreeImage_GetRedMask(dib) == FI16_565_RED_MASK) && (FreeImage_GetGreenMask(dib) == FI16_565_GREEN_MASK) && (FreeImage_GetBlueMask(dib) == FI16_565_BLUE_MASK)) {
                        *guid_format = GUID_PKPixelFormat16bppRGB565;
                    } else {
                        // includes case where all the masks are 0
                        *guid_format = GUID_PKPixelFormat16bppRGB555;
                    }
                    break;
#if FREEIMAGE_COLORORDER == FREEIMAGE_COLORORDER_BGR
                case 24:
                    *guid_format = GUID_PKPixelFormat24bppBGR;
                    break;
                case 32:
                    *guid_format = GUID_PKPixelFormat32bppBGRA;
                    *bHasAlpha = TRUE;
                    break;
#elif FREEIMAGE_COLORORDER == FREEIMAGE_COLORORDER_RGB
                case 24:
                    *guid_format = GUID_PKPixelFormat24bppRGB;
                    break;
                case 32:
                    *guid_format = GUID_PKPixelFormat32bppRGBA;
                    *bHasAlpha = TRUE;
                    break;
#endif
                case 4:
                default:
                    // not supported
                    break;
            }
            break;
        case FIT_UINT16:    // array of unsigned short  : unsigned 16-bit
            *guid_format = GUID_PKPixelFormat16bppGray;
            break;
        case FIT_FLOAT:     // array of float           : 32-bit IEEE floating point
            *guid_format = GUID_PKPixelFormat32bppGrayFloat;
            break;
        case FIT_RGB16:     // 48-bit RGB image         : 3 x 16-bit
            *guid_format = GUID_PKPixelFormat48bppRGB;
            break;
        case FIT_RGBA16:    // 64-bit RGBA image        : 4 x 16-bit
            *guid_format = GUID_PKPixelFormat64bppRGBA;
            *bHasAlpha = TRUE;
            break;
        case FIT_RGBF:      // 96-bit RGB float image   : 3 x 32-bit IEEE floating point
            *guid_format = GUID_PKPixelFormat96bppRGBFloat;
            break;
        case FIT_RGBAF:     // 128-bit RGBA float image : 4 x 32-bit IEEE floating point
            *guid_format = GUID_PKPixelFormat128bppRGBAFloat;
            *bHasAlpha = TRUE;
            break;

        case FIT_INT16:     // array of short           : signed 16-bit
        case FIT_UINT32:    // array of unsigned long   : unsigned 32-bit
        case FIT_INT32:     // array of long            : signed 32-bit
        case FIT_DOUBLE:    // array of double          : 64-bit IEEE floating point
        case FIT_COMPLEX:   // array of FICOMPLEX       : 2 x 64-bit IEEE floating point

        default:
            // unsupported format
            break;
    }

    return (*guid_format != GUID_PKPixelFormatDontCare) ? WMP_errSuccess : WMP_errUnsupportedFormat;
}

// ==========================================================
// Metadata loading
// ==========================================================

/**
   Read a JPEG-XR IFD as a buffer
   @see ReadMetadata
*/
static ERR
ReadProfile(WMPStream* pStream, unsigned cbByteCount, unsigned uOffset, BYTE **ppbProfile) {
    // (re-)allocate profile buffer
    BYTE *pbProfile = *ppbProfile;
    pbProfile = (BYTE*)realloc(pbProfile, cbByteCount);
    if(!pbProfile) {
        return WMP_errOutOfMemory;
    }
    // read the profile
    if(WMP_errSuccess == pStream->SetPos(pStream, uOffset)) {
        if(WMP_errSuccess == pStream->Read(pStream, pbProfile, cbByteCount)) {
            *ppbProfile = pbProfile;
            return WMP_errSuccess;
        }
    }
    return WMP_errFileIO;
}

/**
   Convert a DPKPROPVARIANT to a FITAG, then store the tag as FIMD_EXIF_MAIN
   @see ReadDescriptiveMetadata
*/
static BOOL
ReadPropVariant(WORD tag_id, const DPKPROPVARIANT & varSrc, FIBITMAP *dib) {
    DWORD dwSize;

    if(varSrc.vt == DPKVT_EMPTY) {
        return FALSE;
    }

    // get the tag key
    TagLib& s = TagLib::instance();
    const char *key = s.getTagFieldName(TagLib::EXIF_MAIN, tag_id, NULL);
    if(!key) {
        return FALSE;
    }

    // create a tag
    FITAG *tag = FreeImage_CreateTag();
    if(tag) {
        // set tag ID
        FreeImage_SetTagID(tag, tag_id);
        // set tag type, count, length and value
        switch (varSrc.vt) {
            case DPKVT_LPSTR:
                FreeImage_SetTagType(tag, FIDT_ASCII);
                dwSize = (DWORD)strlen(varSrc.VT.pszVal) + 1;
                FreeImage_SetTagCount(tag, dwSize);
                FreeImage_SetTagLength(tag, dwSize);
                FreeImage_SetTagValue(tag, varSrc.VT.pszVal);
                break;

            case DPKVT_LPWSTR:
                FreeImage_SetTagType(tag, FIDT_UNDEFINED);
                dwSize = (DWORD)(sizeof(U16) * (wcslen((wchar_t *) varSrc.VT.pwszVal) + 1)); // +1 for NULL term
                FreeImage_SetTagCount(tag, dwSize);
                FreeImage_SetTagLength(tag, dwSize);
                FreeImage_SetTagValue(tag, varSrc.VT.pwszVal);
                break;

            case DPKVT_UI2:
                FreeImage_SetTagType(tag, FIDT_SHORT);
                FreeImage_SetTagCount(tag, 1);
                FreeImage_SetTagLength(tag, 2);
                FreeImage_SetTagValue(tag, &varSrc.VT.uiVal);
                break;

            case DPKVT_UI4:
                FreeImage_SetTagType(tag, FIDT_LONG);
                FreeImage_SetTagCount(tag, 1);
                FreeImage_SetTagLength(tag, 4);
                FreeImage_SetTagValue(tag, &varSrc.VT.ulVal);
                break;

            default:
                assert(FALSE); // This case is not handled
                break;
        }
        // get the tag desctiption
        const char *description = s.getTagDescription(TagLib::EXIF_MAIN, tag_id);
        FreeImage_SetTagDescription(tag, description);

        // store the tag
        FreeImage_SetMetadata(FIMD_EXIF_MAIN, dib, key, tag);

        FreeImage_DeleteTag(tag);
    }
    return TRUE;
}

/**
   Read JPEG-XR descriptive metadata and store as EXIF_MAIN metadata
   @see ReadPropVariant
*/
static ERR
ReadDescriptiveMetadata(PKImageDecode *pID, FIBITMAP *dib) {
    // get Exif TIFF metadata
    const DESCRIPTIVEMETADATA *pDescMetadata = &pID->WMP.sDescMetadata;
    // convert metadata to FITAG and store into the EXIF_MAIN metadata model
    ReadPropVariant(WMP_tagImageDescription, pDescMetadata->pvarImageDescription, dib);
    ReadPropVariant(WMP_tagCameraMake, pDescMetadata->pvarCameraMake, dib);
    ReadPropVariant(WMP_tagCameraModel, pDescMetadata->pvarCameraModel, dib);
    ReadPropVariant(WMP_tagSoftware, pDescMetadata->pvarSoftware, dib);
    ReadPropVariant(WMP_tagDateTime, pDescMetadata->pvarDateTime, dib);
    ReadPropVariant(WMP_tagArtist, pDescMetadata->pvarArtist, dib);
    ReadPropVariant(WMP_tagCopyright, pDescMetadata->pvarCopyright, dib);
    ReadPropVariant(WMP_tagRatingStars, pDescMetadata->pvarRatingStars, dib);
    ReadPropVariant(WMP_tagRatingValue, pDescMetadata->pvarRatingValue, dib);
    ReadPropVariant(WMP_tagCaption, pDescMetadata->pvarCaption, dib);
    ReadPropVariant(WMP_tagDocumentName, pDescMetadata->pvarDocumentName, dib);
    ReadPropVariant(WMP_tagPageName, pDescMetadata->pvarPageName, dib);
    ReadPropVariant(WMP_tagPageNumber, pDescMetadata->pvarPageNumber, dib);
    ReadPropVariant(WMP_tagHostComputer, pDescMetadata->pvarHostComputer, dib);
    return WMP_errSuccess;
}

/**
   Read ICC, XMP, Exif, Exif-GPS, IPTC, descriptive (i.e. Exif-TIFF) metadata
   @see ReadProfile, ReadDescriptiveMetadata
*/
static ERR
ReadMetadata(PKImageDecode *pID, FIBITMAP *dib) {
    ERR error_code = 0;     // error code as returned by the interface
    size_t currentPos = 0;  // current stream position

    WMPStream *pStream = pID->pStream;
    WmpDEMisc *wmiDEMisc = &pID->WMP.wmiDEMisc;
    BYTE *pbProfile = NULL;

    try {
        // save current position
        error_code = pStream->GetPos(pStream, &currentPos);
        JXR_CHECK(error_code);

        // ICC profile
        if(0 != wmiDEMisc->uColorProfileByteCount) {
            unsigned cbByteCount = wmiDEMisc->uColorProfileByteCount;
            unsigned uOffset = wmiDEMisc->uColorProfileOffset;
            error_code = ReadProfile(pStream, cbByteCount, uOffset, &pbProfile);
            JXR_CHECK(error_code);
            FreeImage_CreateICCProfile(dib, pbProfile, cbByteCount);
        }

        // XMP metadata
        if(0 != wmiDEMisc->uXMPMetadataByteCount) {
            unsigned cbByteCount = wmiDEMisc->uXMPMetadataByteCount;
            unsigned uOffset = wmiDEMisc->uXMPMetadataOffset;
            error_code = ReadProfile(pStream, cbByteCount, uOffset, &pbProfile);
            JXR_CHECK(error_code);
            // store the tag as XMP
            FITAG *tag = FreeImage_CreateTag();
            if(tag) {
                FreeImage_SetTagLength(tag, cbByteCount);
                FreeImage_SetTagCount(tag, cbByteCount);
                FreeImage_SetTagType(tag, FIDT_ASCII);
                FreeImage_SetTagValue(tag, pbProfile);
                FreeImage_SetTagKey(tag, g_TagLib_XMPFieldName);
                FreeImage_SetMetadata(FIMD_XMP, dib, FreeImage_GetTagKey(tag), tag);
                FreeImage_DeleteTag(tag);
            }
        }

        // IPTC metadata
        if(0 != wmiDEMisc->uIPTCNAAMetadataByteCount) {
            unsigned cbByteCount = wmiDEMisc->uIPTCNAAMetadataByteCount;
            unsigned uOffset = wmiDEMisc->uIPTCNAAMetadataOffset;
            error_code = ReadProfile(pStream, cbByteCount, uOffset, &pbProfile);
            JXR_CHECK(error_code);
            // decode the IPTC profile
            read_iptc_profile(dib, pbProfile, cbByteCount);
        }

        // Exif metadata
        if(0 != wmiDEMisc->uEXIFMetadataByteCount) {
            unsigned cbByteCount = wmiDEMisc->uEXIFMetadataByteCount;
            unsigned uOffset = wmiDEMisc->uEXIFMetadataOffset;
            error_code = ReadProfile(pStream, cbByteCount, uOffset, &pbProfile);
            JXR_CHECK(error_code);
            // decode the Exif profile
            jpegxr_read_exif_profile(dib, pbProfile, cbByteCount, uOffset);
        }

        // Exif-GPS metadata
        if(0 != wmiDEMisc->uGPSInfoMetadataByteCount) {
            unsigned cbByteCount = wmiDEMisc->uGPSInfoMetadataByteCount;
            unsigned uOffset = wmiDEMisc->uGPSInfoMetadataOffset;
            error_code = ReadProfile(pStream, cbByteCount, uOffset, &pbProfile);
            JXR_CHECK(error_code);
            // decode the Exif-GPS profile
            jpegxr_read_exif_gps_profile(dib, pbProfile, cbByteCount, uOffset);
        }

        // free profile buffer
        free(pbProfile);
        // restore initial position
        error_code = pID->pStream->SetPos(pID->pStream, currentPos);
        JXR_CHECK(error_code);

        // as a LAST STEP, read descriptive metadata
        // these metadata overwrite possible identical Exif-TIFF metadata
        // that could have been read inside the Exif IFD

        return ReadDescriptiveMetadata(pID, dib);

    } catch(...) {
        // free profile buffer
        free(pbProfile);
        if(currentPos) {
            // restore initial position
            pStream->SetPos(pStream, currentPos);
        }
        return error_code;
    }
}

// ==========================================================
// Metadata saving
// ==========================================================

/**
   Convert a FITAG (coming from FIMD_EXIF_MAIN) to a DPKPROPVARIANT.
   No allocation is needed here, the function just copy pointers when needed.
   @see WriteDescriptiveMetadata
*/
static BOOL
WritePropVariant(FIBITMAP *dib, WORD tag_id, DPKPROPVARIANT & varDst) {
    FITAG *tag = NULL;

    TagLib& s = TagLib::instance();

    // clear output DPKPROPVARIANT
    varDst.vt = DPKVT_EMPTY;

    // given the tag id, get the tag key
    const char *key = s.getTagFieldName(TagLib::EXIF_MAIN, tag_id, NULL);
    // then, get the tag info
    if(!FreeImage_GetMetadata(FIMD_EXIF_MAIN, dib, key, &tag)) {
        return FALSE;
    }

    // set the tag value
    switch(FreeImage_GetTagType(tag)) {
        case FIDT_ASCII:
            varDst.vt = DPKVT_LPSTR;
            varDst.VT.pszVal = (char*)FreeImage_GetTagValue(tag);
            break;
        case FIDT_BYTE:
        case FIDT_UNDEFINED:
            varDst.vt = DPKVT_LPWSTR;
            varDst.VT.pwszVal = (U16*)FreeImage_GetTagValue(tag);
            break;
        case FIDT_SHORT:
            varDst.vt = DPKVT_UI2;
            varDst.VT.uiVal = *((U16*)FreeImage_GetTagValue(tag));
            break;
        case FIDT_LONG:
            varDst.vt = DPKVT_UI4;
            varDst.VT.ulVal = *((U32*)FreeImage_GetTagValue(tag));
            break;
        default:
            break;
    }

    return TRUE;
}

/**
   Write EXIF_MAIN metadata to JPEG-XR descriptive metadata
   @see WritePropVariant
*/
static ERR
WriteDescriptiveMetadata(PKImageEncode *pIE, FIBITMAP *dib) {
    ERR error_code = 0;     // error code as returned by the interface
    DESCRIPTIVEMETADATA DescMetadata;

    // fill the DESCRIPTIVEMETADATA structure (use pointers to arrays when needed)
    WritePropVariant(dib, WMP_tagImageDescription, DescMetadata.pvarImageDescription);
    WritePropVariant(dib, WMP_tagCameraMake, DescMetadata.pvarCameraMake);
    WritePropVariant(dib, WMP_tagCameraModel, DescMetadata.pvarCameraModel);
    WritePropVariant(dib, WMP_tagSoftware, DescMetadata.pvarSoftware);
    WritePropVariant(dib, WMP_tagDateTime, DescMetadata.pvarDateTime);
    WritePropVariant(dib, WMP_tagArtist, DescMetadata.pvarArtist);
    WritePropVariant(dib, WMP_tagCopyright, DescMetadata.pvarCopyright);
    WritePropVariant(dib, WMP_tagRatingStars, DescMetadata.pvarRatingStars);
    WritePropVariant(dib, WMP_tagRatingValue, DescMetadata.pvarRatingValue);
    WritePropVariant(dib, WMP_tagCaption, DescMetadata.pvarCaption);
    WritePropVariant(dib, WMP_tagDocumentName, DescMetadata.pvarDocumentName);
    WritePropVariant(dib, WMP_tagPageName, DescMetadata.pvarPageName);
    WritePropVariant(dib, WMP_tagPageNumber, DescMetadata.pvarPageNumber);
    WritePropVariant(dib, WMP_tagHostComputer, DescMetadata.pvarHostComputer);

    // copy the structure to the encoder
    error_code = pIE->SetDescriptiveMetadata(pIE, &DescMetadata);

    // no need to free anything here
    return error_code;
}

/**
   Write ICC, XMP, Exif, Exif-GPS, IPTC, descriptive (i.e. Exif-TIFF) metadata
*/
static ERR
WriteMetadata(PKImageEncode *pIE, FIBITMAP *dib) {
    ERR error_code = 0;     // error code as returned by the interface
    BYTE *profile = NULL;
    unsigned profile_size = 0;

    try {
        // write ICC profile
        {
            FIICCPROFILE *iccProfile = FreeImage_GetICCProfile(dib);
            if(iccProfile->data) {
                error_code = pIE->SetColorContext(pIE, (U8*)iccProfile->data, iccProfile->size);
                JXR_CHECK(error_code);
            }
        }

        // write descriptive metadata
        if(FreeImage_GetMetadataCount(FIMD_EXIF_MAIN, dib)) {
            error_code = WriteDescriptiveMetadata(pIE, dib);
            JXR_CHECK(error_code);
        }

        // write IPTC metadata
        if(FreeImage_GetMetadataCount(FIMD_IPTC, dib)) {
            // create a binary profile
            if(write_iptc_profile(dib, &profile, &profile_size)) {
                // write the profile
                error_code = PKImageEncode_SetIPTCNAAMetadata_WMP(pIE, profile, profile_size);
                JXR_CHECK(error_code);
                // release profile
                free(profile);
                profile = NULL;
            }
        }

        // write XMP metadata
        {
            FITAG *tag_xmp = NULL;
            if(FreeImage_GetMetadata(FIMD_XMP, dib, g_TagLib_XMPFieldName, &tag_xmp)) {
                error_code = PKImageEncode_SetXMPMetadata_WMP(pIE, (BYTE*)FreeImage_GetTagValue(tag_xmp), FreeImage_GetTagLength(tag_xmp));
                JXR_CHECK(error_code);
            }
        }

        // write Exif metadata
        {
            if(tiff_get_ifd_profile(dib, FIMD_EXIF_EXIF, &profile, &profile_size)) {
                error_code = PKImageEncode_SetEXIFMetadata_WMP(pIE, profile, profile_size);
                JXR_CHECK(error_code);
                // release profile
                free(profile);
                profile = NULL;
            }
        }

        // write Exif GPS metadata
        {
            if(tiff_get_ifd_profile(dib, FIMD_EXIF_GPS, &profile, &profile_size)) {
                error_code = PKImageEncode_SetGPSInfoMetadata_WMP(pIE, profile, profile_size);
                JXR_CHECK(error_code);
                // release profile
                free(profile);
                profile = NULL;
            }
        }

        return WMP_errSuccess;

    } catch(...) {
        free(profile);
        return error_code;
    }
}



// ==========================================================
// Quantization tables (Y, U, V, YHP, UHP, VHP),
// optimized for PSNR
// ==========================================================

static const int DPK_QPS_420[11][6] = {      // for 8 bit only
    { 66, 65, 70, 72, 72, 77 },
    { 59, 58, 63, 64, 63, 68 },
    { 52, 51, 57, 56, 56, 61 },
    { 48, 48, 54, 51, 50, 55 },
    { 43, 44, 48, 46, 46, 49 },
    { 37, 37, 42, 38, 38, 43 },
    { 26, 28, 31, 27, 28, 31 },
    { 16, 17, 22, 16, 17, 21 },
    { 10, 11, 13, 10, 10, 13 },
    {  5,  5,  6,  5,  5,  6 },
    {  2,  2,  3,  2,  2,  2 }
};

static const int DPK_QPS_8[12][6] = {
    { 67, 79, 86, 72, 90, 98 },
    { 59, 74, 80, 64, 83, 89 },
    { 53, 68, 75, 57, 76, 83 },
    { 49, 64, 71, 53, 70, 77 },
    { 45, 60, 67, 48, 67, 74 },
    { 40, 56, 62, 42, 59, 66 },
    { 33, 49, 55, 35, 51, 58 },
    { 27, 44, 49, 28, 45, 50 },
    { 20, 36, 42, 20, 38, 44 },
    { 13, 27, 34, 13, 28, 34 },
    {  7, 17, 21,  8, 17, 21 }, // Photoshop 100%
    {  2,  5,  6,  2,  5,  6 }
};

static const int DPK_QPS_16[11][6] = {
    { 197, 203, 210, 202, 207, 213 },
    { 174, 188, 193, 180, 189, 196 },
    { 152, 167, 173, 156, 169, 174 },
    { 135, 152, 157, 137, 153, 158 },
    { 119, 137, 141, 119, 138, 142 },
    { 102, 120, 125, 100, 120, 124 },
    {  82,  98, 104,  79,  98, 103 },
    {  60,  76,  81,  58,  76,  81 },
    {  39,  52,  58,  36,  52,  58 },
    {  16,  27,  33,  14,  27,  33 },
    {   5,   8,   9,   4,   7,   8 }
};

static const int DPK_QPS_16f[11][6] = {
    { 148, 177, 171, 165, 187, 191 },
    { 133, 155, 153, 147, 172, 181 },
    { 114, 133, 138, 130, 157, 167 },
    {  97, 118, 120, 109, 137, 144 },
    {  76,  98, 103,  85, 115, 121 },
    {  63,  86,  91,  62,  96,  99 },
    {  46,  68,  71,  43,  73,  75 },
    {  29,  48,  52,  27,  48,  51 },
    {  16,  30,  35,  14,  29,  34 },
    {   8,  14,  17,   7,  13,  17 },
    {   3,   5,   7,   3,   5,   6 }
};

static const int DPK_QPS_32f[11][6] = {
    { 194, 206, 209, 204, 211, 217 },
    { 175, 187, 196, 186, 193, 205 },
    { 157, 170, 177, 167, 180, 190 },
    { 133, 152, 156, 144, 163, 168 },
    { 116, 138, 142, 117, 143, 148 },
    {  98, 120, 123,  96, 123, 126 },
    {  80,  99, 102,  78,  99, 102 },
    {  65,  79,  84,  63,  79,  84 },
    {  48,  61,  67,  45,  60,  66 },
    {  27,  41,  46,  24,  40,  45 },
    {   3,  22,  24,   2,  21,  22 }
};

// ==========================================================
// Plugin Implementation
// ==========================================================

static const char * DLL_CALLCONV
Format() {
    return "JPEG-XR";
}

static const char * DLL_CALLCONV
Description() {
    return "JPEG XR image format";
}

static const char * DLL_CALLCONV
Extension() {
    return "jxr,wdp,hdp";
}

static const char * DLL_CALLCONV
RegExpr() {
    return NULL;
}

static const char * DLL_CALLCONV
MimeType() {
    return "image/vnd.ms-photo";
}

static BOOL DLL_CALLCONV
Validate(FreeImageIO *io, fi_handle handle) {
    BYTE jxr_signature[3] = { 0x49, 0x49, 0xBC };
    BYTE signature[3] = { 0, 0, 0 };

    io->read_proc(&signature, 1, 3, handle);

    return (memcmp(jxr_signature, signature, 3) == 0);
}

static BOOL DLL_CALLCONV
SupportsExportDepth(int depth) {
    return (
        (depth == 1)  ||
        (depth == 8)  ||
        (depth == 16) ||
        (depth == 24) ||
        (depth == 32)
            );
}

static BOOL DLL_CALLCONV
SupportsExportType(FREE_IMAGE_TYPE type) {
    return (
        (type == FIT_BITMAP) ||
        (type == FIT_UINT16) ||
        (type == FIT_RGB16)  ||
        (type == FIT_RGBA16) ||
        (type == FIT_FLOAT)  ||
        (type == FIT_RGBF)   ||
        (type == FIT_RGBAF)
            );
}

static BOOL DLL_CALLCONV
SupportsICCProfiles() {
    return TRUE;
}

static BOOL DLL_CALLCONV
SupportsNoPixels() {
    return TRUE;
}

// ==========================================================
//  Open & Close
// ==========================================================

static void * DLL_CALLCONV
Open(FreeImageIO *io, fi_handle handle, BOOL read) {
    WMPStream *pStream = NULL;  // stream interface
    if(io && handle) {
        // allocate the FreeImageIO stream wrapper
        FreeImageJXRIO *jxr_io = (FreeImageJXRIO*)malloc(sizeof(FreeImageJXRIO));
        if(jxr_io) {
            jxr_io->io = io;
            jxr_io->handle = handle;
            // create a JXR stream wrapper
            if(_jxr_io_Create(&pStream, jxr_io) != WMP_errSuccess) {
                free(jxr_io);
                return NULL;
            }
        }
    }
    return pStream;
}

static void DLL_CALLCONV
Close(FreeImageIO *io, fi_handle handle, void *data) {
    WMPStream *pStream = (WMPStream*)data;
    if(pStream) {
        // free the FreeImageIO stream wrapper
        FreeImageJXRIO *jxr_io = (FreeImageJXRIO*)pStream->state.pvObj;
        free(jxr_io);
        // free the JXR stream wrapper
        pStream->fMem = TRUE;
        _jxr_io_Close(&pStream);
    }
}

// ==========================================================
//  Load
// ==========================================================

/**
   Set decoder parameters
   @param pDecoder Decoder handle
   @param flags FreeImage load flags
*/
static void
SetDecoderParameters(PKImageDecode *pDecoder, int flags) {
    // load image & alpha for formats with alpha
    pDecoder->WMP.wmiSCP.uAlphaMode = 2;
    // more options to come ...
}

/**
   Copy or convert & copy decoded pixels into the dib
   @param pDecoder Decoder handle
   @param out_guid_format Target guid format
   @param dib Output dib
   @param width Image width
   @param height Image height
   @return Returns 0 if successful, returns ERR otherwise
*/
static ERR
CopyPixels(PKImageDecode *pDecoder, PKPixelFormatGUID out_guid_format, FIBITMAP *dib, int width, int height) {
    PKFormatConverter *pConverter = NULL;   // pixel format converter
    ERR error_code = 0; // error code as returned by the interface
    BYTE *pb = NULL;    // local buffer used for pixel format conversion

    // image dimensions
    const PKRect rect = {0, 0, width, height};

    try {
        // get input file pixel format ...
        PKPixelFormatGUID in_guid_format;
        error_code = pDecoder->GetPixelFormat(pDecoder, &in_guid_format);
        JXR_CHECK(error_code);

        // is a format conversion needed ?

        if(IsEqualGUID(out_guid_format, in_guid_format)) {
            // no conversion, load bytes "as is" ...

            // get a pointer to dst pixel data
            BYTE *dib_bits = FreeImage_GetBits(dib);

            // get dst pitch (count of BYTE for stride)
            const unsigned cbStride = FreeImage_GetPitch(dib);

            // decode and copy bits to dst array
            error_code = pDecoder->Copy(pDecoder, &rect, dib_bits, cbStride);
            JXR_CHECK(error_code);
        }
        else {
            // we need to use the conversion API ...

            // allocate the pixel format converter
            error_code = PKCodecFactory_CreateFormatConverter(&pConverter);
            JXR_CHECK(error_code);

            // set the conversion function
            error_code = pConverter->Initialize(pConverter, pDecoder, NULL, out_guid_format);
            JXR_CHECK(error_code);

            // get the maximum stride
            unsigned cbStride = 0;
            {
                PKPixelInfo pPIFrom;
                PKPixelInfo pPITo;

                pPIFrom.pGUIDPixFmt = &in_guid_format;
                error_code = PixelFormatLookup(&pPIFrom, LOOKUP_FORWARD);
                JXR_CHECK(error_code);

                pPITo.pGUIDPixFmt = &out_guid_format;
                error_code = PixelFormatLookup(&pPITo, LOOKUP_FORWARD);
                JXR_CHECK(error_code);

                unsigned cbStrideFrom = ((pPIFrom.cbitUnit + 7) >> 3) * width;
                unsigned cbStrideTo = ((pPITo.cbitUnit + 7) >> 3) * width;
                cbStride = MAX(cbStrideFrom, cbStrideTo);
            }

            // allocate a local decoder / encoder buffer
            error_code = PKAllocAligned((void **) &pb, cbStride * height, 128);
            JXR_CHECK(error_code);

            // copy / convert pixels
            error_code = pConverter->Copy(pConverter, &rect, pb, cbStride);
            JXR_CHECK(error_code);

            // now copy pixels into the dib
            const size_t line_size = FreeImage_GetLine(dib);
            for(int y = 0; y < height; y++) {
                BYTE *src_bits = (BYTE*)(pb + y * cbStride);
                BYTE *dst_bits = (BYTE*)FreeImage_GetScanLine(dib, y);
                memcpy(dst_bits, src_bits, line_size);
            }

            // free the local buffer
            PKFreeAligned((void **) &pb);

            // free the pixel format converter
            PKFormatConverter_Release(&pConverter);
        }

        // FreeImage DIB are upside-down relative to usual graphic conventions
        FreeImage_FlipVertical(dib);

        // post-processing ...
        // -------------------

        // swap RGB as needed

#if FREEIMAGE_COLORORDER == FREEIMAGE_COLORORDER_BGR
        if(IsEqualGUID(out_guid_format, GUID_PKPixelFormat24bppRGB) || IsEqualGUID(out_guid_format, GUID_PKPixelFormat32bppRGB)) {
            SwapRedBlue32(dib);
        }
#elif FREEIMAGE_COLORORDER == FREEIMAGE_COLORORDER_RGB
        if(IsEqualGUID(out_guid_format, GUID_PKPixelFormat24bppBGR) || IsEqualGUID(out_guid_format, GUID_PKPixelFormat32bppBGR)) {
            SwapRedBlue32(dib);
        }
#endif

        return WMP_errSuccess;

    } catch(...) {
        // free the local buffer
        PKFreeAligned((void **) &pb);
        // free the pixel format converter
        PKFormatConverter_Release(&pConverter);

        return error_code;
    }
}

// --------------------------------------------------------------------------

static FIBITMAP * DLL_CALLCONV
Load(FreeImageIO *io, fi_handle handle, int page, int flags, void *data) {
    PKImageDecode *pDecoder = NULL; // decoder interface
    ERR error_code = 0;             // error code as returned by the interface
    PKPixelFormatGUID guid_format;  // loaded pixel format (== input file pixel format if no conversion needed)

    FREE_IMAGE_TYPE image_type = FIT_UNKNOWN;   // input image type
    unsigned bpp = 0;                           // input image bit depth
    FIBITMAP *dib = NULL;

    // get the I/O stream wrapper
    WMPStream *pDecodeStream = (WMPStream*)data;

    if(!handle || !pDecodeStream) {
        return NULL;
    }

    BOOL header_only = (flags & FIF_LOAD_NOPIXELS) == FIF_LOAD_NOPIXELS;

    try {
        int width, height;  // image dimensions (in pixels)

        // create a JXR decoder interface and initialize function pointers with *_WMP functions
        error_code = PKImageDecode_Create_WMP(&pDecoder);
        JXR_CHECK(error_code);

        // attach the stream to the decoder ...
        // ... then read the image container and the metadata
        error_code = pDecoder->Initialize(pDecoder, pDecodeStream);
        JXR_CHECK(error_code);

        // set decoder parameters
        SetDecoderParameters(pDecoder, flags);

        // get dst image format specifications
        unsigned red_mask = 0, green_mask = 0, blue_mask = 0;
        error_code = GetInputPixelFormat(pDecoder, &guid_format, &image_type, &bpp, &red_mask, &green_mask, &blue_mask);
        JXR_CHECK(error_code);

        // get image dimensions
        pDecoder->GetSize(pDecoder, &width, &height);

        // allocate dst image
        {
            dib = FreeImage_AllocateHeaderT(header_only, image_type, width, height, bpp, red_mask, green_mask, blue_mask);
            if(!dib) {
                throw FI_MSG_ERROR_DIB_MEMORY;
            }
            if(FreeImage_GetBPP(dib) == 1) {
                // BD_1 - build a FIC_MINISBLACK palette
                RGBQUAD *pal = FreeImage_GetPalette(dib);
                pal[0].rgbRed = pal[0].rgbGreen = pal[0].rgbBlue = 0;
                pal[1].rgbRed = pal[1].rgbGreen = pal[1].rgbBlue = 255;
            }
        }

        // get image resolution
        {
            float resX, resY;   // image resolution (in dots per inch)
            // convert from English units, i.e. dots per inch to universal units, i.e. dots per meter
            pDecoder->GetResolution(pDecoder, &resX, &resY);
            FreeImage_SetDotsPerMeterX(dib, (unsigned)(resX / 0.0254F + 0.5F));
            FreeImage_SetDotsPerMeterY(dib, (unsigned)(resY / 0.0254F + 0.5F));
        }

        // get metadata & ICC profile
        error_code = ReadMetadata(pDecoder, dib);
        JXR_CHECK(error_code);

        if(header_only) {
            // header only mode ...

            // free the decoder
            pDecoder->Release(&pDecoder);
            assert(pDecoder == NULL);

            return dib;
        }

        // copy pixels into the dib, perform pixel conversion if needed
        error_code = CopyPixels(pDecoder, guid_format, dib, width, height);
        JXR_CHECK(error_code);

        // free the decoder
        pDecoder->Release(&pDecoder);
        assert(pDecoder == NULL);

        return dib;

    } catch (const char *message) {
        // unload the dib
        FreeImage_Unload(dib);
        // free the decoder
        pDecoder->Release(&pDecoder);

        if(NULL != message) {
            FreeImage_OutputMessageProc(s_format_id, message);
        }
    }

    return NULL;
}

// ==========================================================
//  Save
// ==========================================================

/**
   Configure compression parameters

   ImageQuality  Q (BD==1)  Q (BD==8)   Q (BD==16)  Q (BD==32F) Subsample   Overlap
   [0.0, 0.4]    8-IQ*5     (see table) (see table) (see table) 4:4:4       2
   (0.4, 0.8)    8-IQ*5     (see table) (see table) (see table) 4:4:4       1
   [0.8, 1.0)    8-IQ*5     (see table) (see table) (see table) 4:4:4       1
   [1.0, 1.0]    1          1           1           1           4:4:4       0

   @param wmiSCP Encoder parameters
   @param pixelInfo Image specifications
   @param fltImageQuality Image output quality in [0..1), 1 means lossless
*/
static void
SetCompression(CWMIStrCodecParam *wmiSCP, const PKPixelInfo *pixelInfo, float fltImageQuality) {
    if(fltImageQuality < 1.0F) {
        // overlap
        if(fltImageQuality >= 0.5F) {
            wmiSCP->olOverlap = OL_ONE;
        } else {
            wmiSCP->olOverlap = OL_TWO;
        }
        // chroma sub-sampling
        if(fltImageQuality >= 0.5F || pixelInfo->uBitsPerSample > 8) {
            wmiSCP->cfColorFormat = YUV_444;
        } else {
            wmiSCP->cfColorFormat = YUV_420;
        }

        // bit depth
        if(pixelInfo->bdBitDepth == BD_1) {
            wmiSCP->uiDefaultQPIndex = (U8)(8 - 5.0F * fltImageQuality + 0.5F);
        }
        else {
            // remap [0.8, 0.866, 0.933, 1.0] to [0.8, 0.9, 1.0, 1.1]
            // to use 8-bit DPK QP table (0.933 == Photoshop JPEG 100)
            if(fltImageQuality > 0.8F && pixelInfo->bdBitDepth == BD_8 && wmiSCP->cfColorFormat != YUV_420 && wmiSCP->cfColorFormat != YUV_422) {
                fltImageQuality = 0.8F + (fltImageQuality - 0.8F) * 1.5F;
            }

            const int qi = (int) (10.0F * fltImageQuality);
            const float qf = 10.0F * fltImageQuality - (float)qi;

            const int *pQPs =
                    (wmiSCP->cfColorFormat == YUV_420 || wmiSCP->cfColorFormat == YUV_422) ?
                    DPK_QPS_420[qi] :
                    (pixelInfo->bdBitDepth == BD_8 ? DPK_QPS_8[qi] :
                     (pixelInfo->bdBitDepth == BD_16 ? DPK_QPS_16[qi] :
                      (pixelInfo->bdBitDepth == BD_16F ? DPK_QPS_16f[qi] :
                       DPK_QPS_32f[qi])));

            wmiSCP->uiDefaultQPIndex = (U8) (0.5F + (float) pQPs[0] * (1.0F - qf) + (float) (pQPs + 6)[0] * qf);
            wmiSCP->uiDefaultQPIndexU = (U8) (0.5F + (float) pQPs[1] * (1.0F - qf) + (float) (pQPs + 6)[1] * qf);
            wmiSCP->uiDefaultQPIndexV = (U8) (0.5F + (float) pQPs[2] * (1.0F - qf) + (float) (pQPs + 6)[2] * qf);
            wmiSCP->uiDefaultQPIndexYHP = (U8) (0.5F + (float) pQPs[3] * (1.0F - qf) + (float) (pQPs + 6)[3] * qf);
            wmiSCP->uiDefaultQPIndexUHP = (U8) (0.5F + (float) pQPs[4] * (1.0F - qf) + (float) (pQPs + 6)[4] * qf);
            wmiSCP->uiDefaultQPIndexVHP = (U8) (0.5F + (float) pQPs[5] * (1.0F - qf) + (float) (pQPs + 6)[5] * qf);
        }
    } // fltImageQuality < 1.0F
    else {
        // lossless mode
        wmiSCP->uiDefaultQPIndex = 1;
    }
}

/**
   Set encoder parameters
   @param wmiSCP Encoder parameters
   @param pixelInfo Image specifications
   @param flags FreeImage save flags
   @param bHasAlpha TRUE if an alpha layer is present
*/
static void
SetEncoderParameters(CWMIStrCodecParam *wmiSCP, const PKPixelInfo *pixelInfo, int flags, BOOL bHasAlpha) {
    float fltImageQuality = 1.0F;

    // all values have been set to zero by the API
    // update default values for some attributes
    wmiSCP->cfColorFormat = YUV_444;        // color format
    wmiSCP->bdBitDepth = BD_LONG;           // internal bit depth
    wmiSCP->bfBitstreamFormat = SPATIAL;    // compressed image data in spatial order
    wmiSCP->bProgressiveMode = FALSE;       // sequential mode
    wmiSCP->olOverlap = OL_ONE;             // single level overlap processing
    wmiSCP->cNumOfSliceMinus1H = 0;         // # of horizontal slices
    wmiSCP->cNumOfSliceMinus1V = 0;         // # of vertical slices
    wmiSCP->sbSubband = SB_ALL;             // keep all subbands
    wmiSCP->uAlphaMode = 0;                 // 0:no alpha 1: alpha only else: something + alpha
    wmiSCP->uiDefaultQPIndex = 1;           // quantization for grey or rgb layer(s), 1: lossless
    wmiSCP->uiDefaultQPIndexAlpha = 1;      // quantization for alpha layer, 1: lossless

    // process the flags
    // -----------------

    // progressive mode
    if((flags & JXR_PROGRESSIVE) == JXR_PROGRESSIVE) {
        // turn on progressive mode (instead of sequential mode)
        wmiSCP->bProgressiveMode = TRUE;
    }

    // quality in [0.01 - 1.0), 1.0 means lossless - default is 0.80
    int quality = flags & 0x7F;
    if(quality == 0) {
        // defaut to 0.80
        fltImageQuality = 0.8F;
    } else if((flags & JXR_LOSSLESS) == JXR_LOSSLESS) {
        fltImageQuality = 1.0F;
    } else {
        quality = (quality >= 100) ? 100 : quality;
        fltImageQuality = quality / 100.0F;
    }
    SetCompression(wmiSCP, pixelInfo, fltImageQuality);

    // alpha compression
    if(bHasAlpha) {
        wmiSCP->uAlphaMode = 2; // encode with a planar alpha channel
    }
}

// --------------------------------------------------------------------------

static BOOL DLL_CALLCONV
Save(FreeImageIO *io, FIBITMAP *dib, fi_handle handle, int page, int flags, void *data) {
    BOOL bIsFlipped = FALSE;        // FreeImage DIB are upside-down relative to usual graphic conventions
    PKPixelFormatGUID guid_format;  // image format
    PKPixelInfo pixelInfo;          // image specifications
    BOOL bHasAlpha = FALSE;         // is alpha layer present ?

    PKImageEncode *pEncoder = NULL;     // encoder interface
    ERR error_code = 0;                 // error code as returned by the interface

    // get the I/O stream wrapper
    WMPStream *pEncodeStream = (WMPStream*)data;

    if(!dib || !handle || !pEncodeStream) {
        return FALSE;
    }

    try {
        // get image dimensions
        unsigned width = FreeImage_GetWidth(dib);
        unsigned height = FreeImage_GetHeight(dib);

        // check JPEG-XR limits
        if((width < MB_WIDTH_PIXEL) || (height < MB_HEIGHT_PIXEL)) {
            FreeImage_OutputMessageProc(s_format_id, "Unsupported image size: width x height = %d x %d", width, height);
            throw (const char*)NULL;
        }

        // get output pixel format
        error_code = GetOutputPixelFormat(dib, &guid_format, &bHasAlpha);
        JXR_CHECK(error_code);
        pixelInfo.pGUIDPixFmt = &guid_format;
        error_code = PixelFormatLookup(&pixelInfo, LOOKUP_FORWARD);
        JXR_CHECK(error_code);

        // create a JXR encoder interface and initialize function pointers with *_WMP functions
        error_code = PKImageEncode_Create_WMP(&pEncoder);
        JXR_CHECK(error_code);

        // attach the stream to the encoder and set all encoder parameters to zero ...
        error_code = pEncoder->Initialize(pEncoder, pEncodeStream, &pEncoder->WMP.wmiSCP, sizeof(CWMIStrCodecParam));
        JXR_CHECK(error_code);

        // ... then configure the encoder
        SetEncoderParameters(&pEncoder->WMP.wmiSCP, &pixelInfo, flags, bHasAlpha);

        // set pixel format
        pEncoder->SetPixelFormat(pEncoder, guid_format);

        // set image size
        pEncoder->SetSize(pEncoder, width, height);

        // set resolution (convert from universal units to English units)
        float resX = (float)(unsigned)(0.5F + 0.0254F * FreeImage_GetDotsPerMeterX(dib));
        float resY = (float)(unsigned)(0.5F + 0.0254F * FreeImage_GetDotsPerMeterY(dib));
        pEncoder->SetResolution(pEncoder, resX, resY);

        // set metadata
        WriteMetadata(pEncoder, dib);

        // write metadata & pixels
        // -----------------------

        // dib coordinates are upside-down relative to usual conventions
        bIsFlipped = FreeImage_FlipVertical(dib);

        // get a pointer to dst pixel data
        BYTE *dib_bits = FreeImage_GetBits(dib);

        // get dst pitch (count of BYTE for stride)
        const unsigned cbStride = FreeImage_GetPitch(dib);

        // write metadata + pixels on output
        error_code = pEncoder->WritePixels(pEncoder, height, dib_bits, cbStride);
        JXR_CHECK(error_code);

        // recover dib coordinates
        FreeImage_FlipVertical(dib);

        // free the encoder
        pEncoder->Release(&pEncoder);
        assert(pEncoder == NULL);

        return TRUE;

    } catch (const char *message) {
        if(bIsFlipped) {
            // recover dib coordinates
            FreeImage_FlipVertical(dib);
        }
        if(pEncoder) {
            // free the encoder
            pEncoder->Release(&pEncoder);
            assert(pEncoder == NULL);
        }
        if(NULL != message) {
            FreeImage_OutputMessageProc(s_format_id, message);
        }
    }

    return FALSE;
}

// ==========================================================
//   Init
// ==========================================================

void DLL_CALLCONV
InitJXR(Plugin *plugin, int format_id) {
    s_format_id = format_id;

    plugin->format_proc = Format;
    plugin->description_proc = Description;
    plugin->extension_proc = Extension;
    plugin->regexpr_proc = RegExpr;
    plugin->open_proc = Open;
    plugin->close_proc = Close;
    plugin->pagecount_proc = NULL;
    plugin->pagecapability_proc = NULL;
    plugin->load_proc = Load;
    plugin->save_proc = Save;
    plugin->validate_proc = Validate;
    plugin->mime_proc = MimeType;
    plugin->supports_export_bpp_proc = SupportsExportDepth;
    plugin->supports_export_type_proc = SupportsExportType;
    plugin->supports_icc_profiles_proc = SupportsICCProfiles;
    plugin->supports_no_pixels_proc = SupportsNoPixels;
}
